/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package downloadmanager.downloader;

import downloadmanager.component.ProgressSegment;
import downloadmanager.component.ProgressSegmentList;
import downloadmanager.event.XEvent;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.net.URL;
import java.util.ArrayList;
import java.util.Date;

import java.util.List;

import java.util.logging.Level;

import java.util.logging.Logger;

import util.Converter;

import util.DateExtensions;

import util.FileExtensions;

/**
 *
 * @author Administrator
 */
public class Downloader {

    public final int MAX_RETRIES = 3;
    public final int RETRY_DELAY = 2;//10 seconds;
    public final int MIN_SEGMENT_LEFT_TO_START_NEW_SEGMENT = 30;
    public final int MIN_SEGMENT_SIZE = 100000;
    public final int MAX_SEGMENT = 16;
    private String localFile;
    private int requestedSegmentCount;
    private URL url;
    private final Object segmentsLock = new Object();
    private List<DownloadSegment> segments = new ArrayList<DownloadSegment>();
    private Thread mainThread;
    private final List<Thread> threads;
    private RemoteFileInfo remoteFileInfo;
    private DownloaderState state;
    private Date createdDateTime;
    private Exception lastError;
    private HttpProtocolProvider protocolProvider = new HttpProtocolProvider();
    private SegmentCalculator segmentCalculator = new SegmentCalculator();
    private String statusMessage;
    public final XEvent Ending = new XEvent();
    public final XEvent InfoReceived = new XEvent();
    public final XEvent<DownloaderState> StateChanged = new XEvent<DownloaderState>();
    public final XEvent RestartingSegment = new XEvent();
    public final XEvent SegmentStoped = new XEvent();
    public final XEvent SegmentStarting = new XEvent();
    public final XEvent SegmentStarted = new XEvent();
    public final XEvent SegmentFailed = new XEvent();
    private final ProgressSegmentList progressSegmentList = new ProgressSegmentList();

    private Downloader(
            URL rl,
            String localFile) {
        threads = new ArrayList<Thread>();
        url = rl;
        this.localFile = localFile;
    }

    public Downloader(
            URL rl,
            String localFile,
            int segmentCount) {
        this(rl, localFile);
        setState(DownloaderState.NeedToPrepare);

        this.createdDateTime = new Date();
        this.requestedSegmentCount = segmentCount;
        this.segments = new ArrayList<DownloadSegment>();
    }

    public Downloader(
            URL rl,
            String localFile,
            List<DownloadSegment> segments,
            RemoteFileInfo remoteInfo,
            int requestedSegmentCount,
            Date createdDateTime) {
        this(rl, localFile);
        if (segments.size() > 0) {
            setState(DownloaderState.Prepared);
        } else {
            setState(DownloaderState.NeedToPrepare);
        }

        this.createdDateTime = createdDateTime;
        setRemoteFileInfo(remoteInfo);
        this.requestedSegmentCount = requestedSegmentCount;
        this.segments = segments;
        invalidateProgressSegmentList();
    }

    /**
     * Sync the progress segment list with current number of segments
     */
    void invalidateProgressSegmentList() {
        synchronized (progressSegmentList) {
            synchronized (segmentsLock) {
                progressSegmentList.clearSegments();
                for (DownloadSegment s : segments) {
                    progressSegmentList.addProgressSegment(new ProgressSegment(s));
                }
            }
        }
    }

    public ProgressSegmentList getProgressSegmentList() {
        return progressSegmentList;
    }

    public URL getURL() {
        return this.url;
    }

    public long getFileSize() {
        if (remoteFileInfo == null) {
            return 0;
        }
        return remoteFileInfo.getFileSize();
    }

    public Date getCreatedDateTime() {
        return createdDateTime;
    }

    public int getRequestedSegments() {
        return requestedSegmentCount;
    }

    public String getLocalFile() {
        return this.localFile;
    }

    public double getProgress() {
        int count = segments.size();

        if (count > 0) {
            double progress = 0;

            for (int i = 0; i < count; i++) {
                progress += segments.get(i).getProgress();
            }

            return progress / count;
        } else {
            return 0;
        }
    }

    public double getRate() {
        double rate = 0;

        for (int i = 0; i < segments.size(); i++) {
            rate += segments.get(i).getRate();
        }

        return rate;
    }

    public long getTransfered() {
        long transfered = 0;

        for (int i = 0; i < segments.size(); i++) {
            transfered += segments.get(i).getTransfered();
        }

        return transfered;
    }

    /**
     *
     * @return number of seconds until download finish
     */
    public long getLeft() {
        if (this.getRate() == 0) {
            return Integer.MAX_VALUE;
        }

        double missingTransfer = 0;

        for (int i = 0; i < segments.size(); i++) {
            missingTransfer += segments.get(
                    i).getMissingTransfer();
        }

        return Converter.toLong(missingTransfer / this.getRate());
    }

    public List<DownloadSegment> getSegments() {
        return segments;
    }

    public int getThreadsCount() {
        return threads.size();
    }

    public Exception getLastError() {
        return lastError;
    }

    public void setLastError(Exception lastError) {
        this.lastError = lastError;
    }

    public DownloaderState getState() {
        return state;
    }

    public boolean isWorking() {
        return (state == DownloaderState.Preparing ||
                state == DownloaderState.WaitingForReconnect ||
                state == DownloaderState.Working);
    }

    public RemoteFileInfo getRemoteFileInfo() {
        return remoteFileInfo;
    }

    private void setRemoteFileInfo(RemoteFileInfo remoteFileInfo) {
        this.remoteFileInfo = remoteFileInfo;
        if (remoteFileInfo != null) {
            progressSegmentList.setMax(remoteFileInfo.getFileSize());
            progressSegmentList.setMin(0);
        }
    }

    public String getStatusMessage() {
        return statusMessage;
    }

    public void setStatusMessage(String statusMessage) {
        this.statusMessage = statusMessage;
    }

    public SegmentCalculator getSegmentCalculator() {
        return segmentCalculator;
    }

    private void setState(DownloaderState value) {
        state = value;

        onStateChanged();
    }

    private void startToPrepare() {
        mainThread = new Thread(new Runnable() {

            public void run() {
                try {
                    startDownloadThreadProc(requestedSegmentCount);
                } catch (InterruptedException ex) {
                    System.out.println(ex);
                }
            }
        });
        mainThread.start();
    }

    private void startPrepared() {
        mainThread = new Thread(new Runnable() {

            public void run() {
                try {
                    restartDownload();
                } catch (InterruptedException ex) {
                    Logger.getLogger(Downloader.class.getName()).log(Level.SEVERE, null, ex);
                }
            }
        });
        mainThread.start();
    }

    protected void onRestartingSegment(DownloadSegment segment) {
        if (RestartingSegment != null) {
            RestartingSegment.fire(this, segment);
        }
    }

    protected void onSegmentStoped(DownloadSegment segment) {
        if (SegmentStoped != null) {
            SegmentStoped.fire(this, segment);
        }
    }

    protected void onSegmentFailed(DownloadSegment segment) {
        if (SegmentFailed != null) {
            SegmentFailed.fire(this, segment);
        }
    }

    protected void onSegmentStarting(DownloadSegment segment) {
        if (SegmentStarting != null) {
            SegmentStarting.fire(this, segment);
        }
    }

    protected void onSegmentStarted(DownloadSegment segment) {
        if (SegmentStarted != null) {
            SegmentStarted.fire(this, segment);
        }
    }

    protected void onStateChanged() {
        StateChanged.fire(this, this.getState());
    }

    protected void onEnding() {
        if (Ending != null) {
            Ending.fire(this, null);
        }
    }

    protected void onInfoReceived() {
        if (InfoReceived != null) {
            InfoReceived.fire(this, null);
        }
    }

    public void waitForConclusion() {
        if (!isWorking()) {
            if (mainThread != null && mainThread.isAlive()) {
                try {
                    mainThread.join(1000);
                } catch (InterruptedException ex) {
                }
            }
        }

        while (isWorking()) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException ex) {
            }
        }

        System.out.println(this.getState().toString());
    }

    public void pause() throws InterruptedException {
        if (state == DownloaderState.Preparing || state == DownloaderState.WaitingForReconnect) {
            synchronized (segmentsLock) {
                segments.clear();
            }
            invalidateProgressSegmentList();

            mainThread.interrupt();
            mainThread = null;
            setState(DownloaderState.NeedToPrepare);
            return;
        }

        if (state == DownloaderState.Working) {
            setState(DownloaderState.Pausing);

            while (!isAllWorkersStopped(5)) {
            }


            synchronized (threads) {
                threads.clear();
            }

            mainThread.interrupt();
            mainThread = null;

            if (remoteFileInfo != null && !remoteFileInfo.isAcceptRanges()) {
                // reset the segment
                segments.get(0).setStartPosition(0);
            }

            setState(DownloaderState.Paused);
        }
    }

    public void start() {
        if (state == DownloaderState.NeedToPrepare) {
            setState(DownloaderState.Preparing);

            startToPrepare();
        } else if (state != DownloaderState.Preparing &&
                state != DownloaderState.Pausing &&
                state != DownloaderState.Working &&
                state != DownloaderState.WaitingForReconnect) {
            setState(DownloaderState.Preparing);

            startPrepared();
        }
    }

    private void startDownloadThreadProc(int segmentCount) throws InterruptedException {
        setState(DownloaderState.Preparing);

        segmentCount = Math.min(segmentCount, MAX_SEGMENT);
        InputStream[] inputStream = new InputStream[1];
        int currentTry = 0;

        do {
            lastError = null;

            if (state == DownloaderState.Pausing) {
                setState(DownloaderState.NeedToPrepare);
                return;
            }

            setState(DownloaderState.Preparing);

            currentTry++;
            try {
                setRemoteFileInfo(protocolProvider.getFileInfo(url, inputStream));
                break;
            } catch (InterruptedException ex) {
                setState(DownloaderState.NeedToPrepare);
                return;
            } catch (Exception ex) {
                lastError = ex;
                if (currentTry < MAX_RETRIES) {
                    setState(DownloaderState.WaitingForReconnect);
                    Thread.sleep(RETRY_DELAY);
                } else {
                    setState(DownloaderState.NeedToPrepare);
                    return;
                }
            }
        } while (true);

        try {
            lastError = null;
            startSegments(segmentCount, inputStream[0]);
        } catch (InterruptedException ex) {
            throw ex;
        } catch (Exception ex) {
            lastError = ex;
            setState(DownloaderState.EndedWithError);
        }
    }

    private void startSegments(int segmentCount, InputStream inputStream) throws Exception {
        // notifies
        onInfoReceived();

        // allocs the file on disk
        localFile = FileExtensions.allocateFile(localFile, getFileSize());

        long segmentSize;

        CalculatedSegment[] calculatedSegments;

        if (!remoteFileInfo.isAcceptRanges()) {
            calculatedSegments = new CalculatedSegment[]{new CalculatedSegment(0, remoteFileInfo.getFileSize())};
        } else {
            calculatedSegments = segmentCalculator.getSegments(segmentCount, remoteFileInfo);
        }

        synchronized (threads) {
            threads.clear();
        }
        synchronized (segmentsLock) {
            segments.clear();
        }

        for (int i = 0; i < calculatedSegments.length; i++) {
            DownloadSegment segment = new DownloadSegment();
            if (i == 0) {
                segment.setInputStream(inputStream);
            }

            segment.setIndex(i);
            segment.setInitialStartPosition(calculatedSegments[i].getStartPosition());
            segment.setStartPosition(calculatedSegments[i].getStartPosition());
            segment.setEndPosition(calculatedSegments[i].getEndPosition());

            segments.add(segment);
        }

        invalidateProgressSegmentList();
        runSegments();
    }

    private void restartDownload() throws InterruptedException {
        int currentTry = 0;
        InputStream[] stream = new InputStream[1];
        RemoteFileInfo newInfo;

        try {
            do {
                lastError = null;

                setState(DownloaderState.Preparing);

                currentTry++;
                try {
                    newInfo = protocolProvider.getFileInfo(this.getURL(), stream);
                    break;
                } catch (Exception ex) {
                    lastError = ex;
                    if (currentTry < MAX_RETRIES) {
                        setState(DownloaderState.WaitingForReconnect);
                        Thread.sleep(RETRY_DELAY * 1000);
                    } else {
                        return;
                    }
                }
            } while (true);
        } finally {
            setState(DownloaderState.Prepared);
        }

        try {
            // check if the file changed on the server
            if (!newInfo.isAcceptRanges() ||
                    newInfo.getLastModified() > remoteFileInfo.getLastModified() ||
                    newInfo.getFileSize() != remoteFileInfo.getFileSize()) {
                setRemoteFileInfo(newInfo);
                startSegments(this.getRequestedSegments(), stream[0]);
            } else {
                if (stream[0] != null) {
                    stream[0].close();
                }

                runSegments();
            }
        } catch (InterruptedException ex) {
            throw ex;
        } catch (Exception ex) {
            lastError = ex;
            setState(DownloaderState.EndedWithError);
        }
    }

    private void runSegments() throws InterruptedException, FileNotFoundException, IOException {
        setState(DownloaderState.Working);

        RandomAccessFile fs = new RandomAccessFile(getLocalFile(), "rw");
        for (int i = 0; i < this.segments.size(); i++) {
            segments.get(i).setOutputStream(fs);
            startSegment(segments.get(i));
        }
        do {
            while (!isAllWorkersStopped(1000)) {
            }
        } while (restartFailedSegments());


        for (int i = 0; i < segments.size(); i++) {
            if (segments.get(i).getState() == DownloadSegmentState.Error) {
                setState(DownloaderState.EndedWithError);
                return;
            }
        }

        if (state != DownloaderState.Pausing) {
            onEnding();
        }

        this.segments.get(0).getOutputStream().close();
        setState(DownloaderState.Ended);
    }

    private boolean restartFailedSegments() throws InterruptedException {
        boolean hasErrors = false;
        double delay = 0;

        for (int i = 0; i < segments.size(); i++) {
            if (segments.get(i).getState() == DownloadSegmentState.Error &&
                    segments.get(i).getLastErrorDateTime() != null &&
                    (MAX_RETRIES == 0 ||
                    segments.get(i).getCurrentTry() < MAX_RETRIES)) {
                hasErrors = true;
                long ts = DateExtensions.getDeltaSeconds(segments.get(i).getLastErrorDateTime(), new Date());

                if (ts >= RETRY_DELAY) {
                    segments.get(i).setCurrentTry(segments.get(i).getCurrentTry() + 1);
                    startSegment(segments.get(i));
                    onRestartingSegment(segments.get(i));
                } else {
                    delay = Math.max(delay, RETRY_DELAY * 1000 - ts * 1000);
                }
            }
        }
        Thread.sleep((long) delay);

        return hasErrors;
    }

    private void startSegment(DownloadSegment newSegment) {
        final DownloadSegment s = newSegment;
        Thread segmentThread = new Thread(new Runnable() {

            public void run() {
                segmentThreadProc(s);
            }
        });
        segmentThread.start();

        synchronized (threads) {
            threads.add(segmentThread);
        }
    }

    private boolean isAllWorkersStopped(int timeOut) throws InterruptedException {
        boolean allFinished = true;


        Thread[] workers;
        synchronized (threads) {
            workers = threads.toArray(new Thread[threads.size()]);
        }

        for (Thread t : workers) {
            t.join(timeOut);
            boolean finished = !(t.isAlive());
            allFinished = allFinished & finished;

            if (finished) {
                synchronized (threads) {
                    threads.remove(t);
                }
            }
        }

        return allFinished;
    }

    private void segmentThreadProc(DownloadSegment segment) {
        segment.setLastError(null);

        try {
            if (segment.getEndPosition() > 0 && segment.getStartPosition() >= segment.getEndPosition()) {
                segment.setState(DownloadSegmentState.Finished);

                // raise the event
                onSegmentStoped(segment);

                return;
            }

            int buffSize = 8192;
            byte[] buffer = new byte[buffSize];

            segment.setState(DownloadSegmentState.Connecting);

            // raise the event
            onSegmentStarting(segment);

            if (segment.getInputStream() == null) {
                // get the next URL (It can the the main url or some mirror)
                URL location = getURL();
                // get the protocol provider for that mirror

                // get the input stream from start position
                segment.setInputStream(protocolProvider.createStream(location, segment.getStartPosition(), segment.getEndPosition()));

            }

            // raise the event
            onSegmentStarted(segment);

            // change the segment state
            segment.setState(DownloadSegmentState.Downloading);
            segment.setCurrentTry(0);

            long readSize;

            do {
                // reads the buffer from input stream
                readSize = segment.getInputStream().read(buffer, 0, buffSize);

                // check if the segment has reached the end
                if (segment.getEndPosition() > 0 &&
                        segment.getStartPosition() + readSize > segment.getEndPosition()) {
                    // adjust the 'readSize' to write only necessary bytes
                    readSize = (segment.getEndPosition() - segment.getStartPosition());
                    if (readSize <= 0) {
                        segment.setStartPosition(segment.getEndPosition());
                        break;
                    }
                }

                // locks the stream to avoid that other threads changes
                // the position of stream while this thread is writing into the stream
                synchronized (segment.getOutputStream()) {
                    segment.getOutputStream().seek(segment.getStartPosition());
                    segment.getOutputStream().write(buffer, 0, (int) readSize);
                }

                // increse the start position of the segment and also calculates the rate
                segment.increaseStartPosition(readSize);

                // check if the stream has reached its end
                if (segment.getEndPosition() > 0 && segment.getStartPosition() >= segment.getEndPosition()) {
                    segment.setStartPosition(segment.getEndPosition());
                    break;
                }

                // check if the user have requested to pause the download
                if (state == DownloaderState.Pausing) {
                    segment.setState(DownloadSegmentState.Paused);
                    break;
                }

                //Thread.Sleep(1500);
            } while (readSize > 0);

            if (segment.getState() == DownloadSegmentState.Downloading) {
                segment.setState(DownloadSegmentState.Finished);

                // try to create other segment,
                // spliting the missing bytes from one existing segment
                addNewSegmentIfNeeded();
            }

            // raise the event
            onSegmentStoped(segment);
        } catch (Exception ex) {
            // store the error information
            segment.setState(DownloadSegmentState.Error);
            segment.setLastError(ex);

            System.out.println(ex.toString());

            // raise the event
            onSegmentFailed(segment);
        } finally {
            // clean up the segment
            segment.setInputStream(null);
        }
    }

    private void addNewSegmentIfNeeded() {
        synchronized (segmentsLock) {
            for (int i = 0; i <
                    segments.size(); i++) {
                DownloadSegment oldSegment = this.segments.get(i);
                if (oldSegment.getState() == DownloadSegmentState.Downloading &&
                        oldSegment.getLeft() > MIN_SEGMENT_LEFT_TO_START_NEW_SEGMENT &&
                        oldSegment.getMissingTransfer() / 2 >= MIN_SEGMENT_SIZE) {
                    // get the half of missing size of oldSegment
                    long newSize = oldSegment.getMissingTransfer() / 2;

                    // create a new segment allocation the half old segment
                    DownloadSegment newSegment = new DownloadSegment();
                    newSegment.setIndex(segments.size());
                    newSegment.setStartPosition(oldSegment.getStartPosition() + newSize);
                    newSegment.setInitialStartPosition(newSegment.getStartPosition());
                    newSegment.setEndPosition(oldSegment.getEndPosition());
                    newSegment.setOutputStream(oldSegment.getOutputStream());

                    // removes bytes from old segments
                    oldSegment.setEndPosition(oldSegment.getEndPosition() - newSize);

                    // add the new segment to the list
                    segments.add(newSegment);

                    startSegment(newSegment);

                    break;

                }

            }
        }
        invalidateProgressSegmentList();
    }
}
